package com.meadhall.sync2cloud;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.activation.MimetypesFileTypeMap;
import javax.swing.JLabel;
import javax.swing.SwingWorker;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.vfs.FileChangeEvent;
import org.apache.commons.vfs.FileListener;
import org.apache.commons.vfs.FileObject;
import org.apache.commons.vfs.FileSelectInfo;
import org.apache.commons.vfs.FileSelector;
import org.apache.commons.vfs.FileSystemException;
import org.apache.commons.vfs.FileSystemManager;
import org.apache.commons.vfs.VFS;
import org.apache.commons.vfs.impl.DefaultFileMonitor;

import com.rackspacecloud.client.cloudfiles.FilesClient;
import com.rackspacecloud.client.cloudfiles.FilesObject;

/**
 * Performs local-to-remote synchronization. Implements FileListener to get updates from Commons VFS FileMonitor. Extends SwingWorker to work as a background task in Swing application.
 * 
 * @author Marshall Levin
 */
public class L2RSync extends SwingWorker<Void, String> implements FileListener {

	private File localRoot;
	private String remoteContainer;
	private FilesClient client;
	private DefaultFileMonitor fileMonitor;
	private FileSystemManager fileSystemManager;
	private FileObject localRootFileObject;
	private JLabel statusLabel;

	/**
	 * Checks parameters and initializes the service.
	 * 
	 * @param localRoot
	 *            specifying the local path
	 * @param remoteContainer
	 *            specifying the remote container
	 * @param client
	 *            a Rackspace CloudFiles client
	 * @param statusLabel
	 *            for displaying status information
	 * @throws FileSyncException
	 */
	public L2RSync(File localRoot, String remoteContainer, FilesClient client, JLabel statusLabel) throws FileSyncException {
		publish("Checking parameters");
		if (localRoot == null || !localRoot.exists() || !localRoot.isDirectory() || !localRoot.canRead()) {
			throw new FileSyncException("Local root directory is not valid");
		}
		if (remoteContainer == null || remoteContainer.trim().equals("")) {
			throw new FileSyncException("Remote container name is not valid");
		}
		if (client == null || !client.isLoggedin()) {
			throw new FileSyncException("Client must be authenticated");
		}
		if (statusLabel == null) {
			throw new FileSyncException("Label element cannot be null");
		}
		this.localRoot = localRoot;
		this.remoteContainer = remoteContainer;
		this.client = client;
		this.fileMonitor = new DefaultFileMonitor(this);
		try {
			this.fileSystemManager = VFS.getManager();
			this.localRootFileObject = this.fileSystemManager.toFileObject(localRoot);
		} catch (FileSystemException fse) {
			throw new FileSyncException("Unable to initialized filesystem", fse);
		}
		this.statusLabel = statusLabel;
		publish("System initialized");
	}

	/**
	 * Kicks off the initial synchronization.
	 * 
	 * @throws FileSyncException
	 */
	public void initialSync() throws FileSyncException {
		publish("Performing initial sync");
		createContainer();
		copyLocalToRemote();
	}

	/**
	 * Copies local files to remote location. Builds a list of files in each location and compares them, copying or deleting as necessary to make the remote location mirror the local one.
	 * 
	 * @throws FileSyncException
	 */
	private void copyLocalToRemote() throws FileSyncException {

		try {

			// Build a map of local and remote files
			publish("Building map of local files");
			FileObject[] localFiles = localRootFileObject.findFiles(new FileSelector() {
				public boolean includeFile(FileSelectInfo fileInfo) {
					File tmpFile;
					try {
						tmpFile = new File(fileInfo.getFile().getURL().getFile());
					} catch (FileSystemException fse) {
						return false;
					}
					return !tmpFile.equals(localRoot);
				}

				public boolean traverseDescendents(FileSelectInfo fileInfo) {
					return true;
				}
			});
			Map<String, File> localFileMap = new HashMap<String, File>();
			for (int i = 0; i < localFiles.length; i++) {
				File tmpFile = new File(localFiles[i].getURL().getFile());
				localFileMap.put(getRemoteStylePath(tmpFile), tmpFile);
			}
			publish("Building map of remote files");
			Collection<FilesObject> remoteFiles = client.listObjects(remoteContainer);
			Map<String, FilesObject> remoteFileMap = new HashMap<String, FilesObject>();
			for (FilesObject file : remoteFiles) {
				remoteFileMap.put(file.getName(), file);
			}

			// For each local file, copy to remote file if different or not present
			publish("Finding files to be copied");
			for (String filename : localFileMap.keySet()) {
				File file = localFileMap.get(filename);
				if (!remoteFileMap.containsKey(filename)) {
					// remote file does not exist
					publish("Copying " + filename + " to server");
					if (file.isDirectory()) {
						// remote file is not present and should be a directory, so create it (and any necessary parent directories as well)
						client.createFullPath(remoteContainer, filename);
					} else {
						if (!file.getParentFile().equals(localRoot)) {
							// create parent directory, but do not attempt to create the root directory
							client.createFullPath(remoteContainer, getRemoteStylePath(file.getParentFile()));
						}
						// remote file is not present and should be a normal file, so simply upload it
						client.storeObjectAs(remoteContainer, file, MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(file), filename);
					}
				} else {
					// remote file exists
					if (file.isDirectory()) {
						// make sure the remote file is also a directory
						client.createFullPath(remoteContainer, filename);
					} else {
						// copy only if different
						String localHash = FilesClient.md5Sum(file);
						String remoteHash = remoteFileMap.get(filename).getMd5sum();
						if (!localHash.equalsIgnoreCase(remoteHash)) {
							if (!file.getParentFile().equals(localRoot)) {
								// create parent directory, but do not attempt to create the root directory
								client.createFullPath(remoteContainer, getRemoteStylePath(file.getParentFile()));
							}
							// copy file
							client.storeObjectAs(remoteContainer, file, MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(file), filename);
						}
					}
				}
			}

			// For each remote file, if not present locally, remove it
			for (String filename : remoteFileMap.keySet()) {
				if (!localFileMap.containsKey(filename)) {
					publish("Removing remote file " + filename);
					client.deleteObject(remoteContainer, filename);
				}
			}

		} catch (Exception e) {
			throw new FileSyncException("Unable to perform initial sync", e);
		}

	}

	/**
	 * Create the remote container if it does not exist.
	 * 
	 * @throws FileSyncException
	 */
	private void createContainer() throws FileSyncException {
		publish("Checking container");
		try {
			boolean exists = client.containerExists(remoteContainer);
			if (!exists) {
				client.createContainer(remoteContainer);
			}
		} catch (Exception e) {
			throw new FileSyncException("Unable to check or create remote container", e);
		}
	}

	/**
	 * Convert a Windows-style path to a UNIX-style path appropriate for CloudFiles.
	 * 
	 * @param file
	 *            specifying the local file object
	 * @return a String representing the path relative to the container
	 */
	private String getRemoteStylePath(File file) {
		String path = null;
		if (file != null) {
			path = file.getAbsolutePath();
			path = FilenameUtils.getPath(path) + FilenameUtils.getName(path);
			path = FilenameUtils.separatorsToUnix(path);
			path = path.substring(localRoot.getName().length() + 1); // omit name of root directory
		}
		return path;
	}

	/**
	 * Listener for changed files. If the file is not a directory, it is copied to the remote location.
	 */
	@Override
	public void fileChanged(FileChangeEvent event) throws Exception {
		File localFile = new File(event.getFile().getURL().getFile());
		if (!localFile.isDirectory()) {
			publish("Copying " + getRemoteStylePath(localFile) + " to server");
			client.storeObjectAs(remoteContainer, localFile, MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(localFile), getRemoteStylePath(localFile));
		}
	}

	/**
	 * Listener for created files. The remote "directory" or file is created.
	 */
	@Override
	public void fileCreated(FileChangeEvent event) throws Exception {
		File localFile = new File(event.getFile().getURL().getFile());
		publish("Copying " + getRemoteStylePath(localFile) + " to server");
		if (!localFile.isDirectory()) {
			client.storeObjectAs(remoteContainer, localFile, MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(localFile), getRemoteStylePath(localFile));
		} else {
			client.createFullPath(remoteContainer, getRemoteStylePath(localFile));
		}
	}

	/**
	 * Listener for deleted files. The corresponding remote object is deleted.
	 */
	@Override
	public void fileDeleted(FileChangeEvent event) throws Exception {
		File localFile = new File(event.getFile().getURL().getFile());
		publish("Removing remote file " + getRemoteStylePath(localFile));
		client.deleteObject(remoteContainer, getRemoteStylePath(localFile));
	}

	/**
	 * SwingWorker method to allow task to be formed in the background. Watches isCancelled() to allow the task to be stopped. Performs the initial synchronization and then starts Commons VFS FileMonitor to watch for local changes.
	 */
	@Override
	protected Void doInBackground() throws Exception {
		initialSync();
		fileMonitor.setRecursive(true);
		fileMonitor.addFile(localRootFileObject);
		fileMonitor.start();
		while (!isCancelled() && !Thread.interrupted()) {
			// just wait!
		}
		this.fileMonitor.stop();
		return null;
	}

	/**
	 * Updates the UI with the status of the system.
	 */
	@Override
	protected void process(List<String> updates) {
		super.process(updates);
		statusLabel.setText(updates.get(updates.size() - 1));
	}

}